//
// PomExplorer
//
// Copyright (C) jextra.net.
//
//  This file is part of the net.jextra.dep Library.
//
//  The net.jextra.dep Library is free software; you can redistribute it and/or
//  modify it under the terms of the GNU Lesser General Public
//  License as published by the Free Software Foundation; either
//  version 2.1 of the License, or (at your option) any later version.
//
//  The net.jextra.dep Library is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
//  Lesser General Public License for more details.
//
//  You should have received a copy of the GNU Lesser General Public
//  License along with the net.jextra.dep Library; if not, write to the Free
//  Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
//  02111-1307 USA.
//

package net.jextra.dep.pom;

import java.io.*;
import java.util.*;
import java.util.regex.*;
import net.jextra.dep.*;
import net.jextra.dep.repo.*;

/**
 * <p>
 * Build a {@link DepTree} from a directory that may included multiple depFiles.
 * </p>
 */
public class PomExplorer
{
    // ============================================================
    // Enums
    // ============================================================

    public enum Scope
    {
        compile( "compile" ),
        test( "test" ),
        runtime( "runtime" ),
        provided( "provided" ),
        system( "system" ),
        importScope( "import" );

        private String name;

        private Scope( String name )
        {
            this.name = name;
        }

        public static Scope find( String value )
        {
            if ( value == null )
            {
                return compile;
            }

            for ( Scope s : values() )
            {
                if ( s.matches( value ) )
                {
                    return s;
                }
            }

            return null;
        }

        public boolean matches( String value )
        {
            return name.equalsIgnoreCase( value );
        }
    }

    public enum Status
    {
        newComponent,
        missingComponent,
        redundantComponent;
    }

    public enum MatchType
    {
        noMatch,
        match,
        suggested,
        exact
    }

    // ============================================================
    // Fields
    // ============================================================

    private static String versionPatternString;
    private static Pattern versionPattern;
    private static String rangePatternString;
    private static Pattern rangePattern;
    private static Pattern pairPattern;
    private static Pattern leftPattern;
    private static Pattern rightPattern;
    private static Pattern allPattern;
    private static Pattern multiRangePattern;

    private String[] scopes;
    private File projectRootDir;
    private DepDirFilter dirFilter;
    private ArrayList<DepRepo> repos;
    private HashSet<String> missingDependencies;
    private boolean verbose;

    private HashMap<String, PomReader.Property> properties;

    private Stack<DepLayer> layers;

    // ============================================================
    // Constructors
    // ============================================================

    static
    {
        versionPatternString = "[0-9]+(?:\\.[0-9a-z]+)*";
        versionPattern = Pattern.compile( versionPatternString, Pattern.CASE_INSENSITIVE );
        rangePatternString = "([\\[\\(])([^\\]\\)]*)([\\]\\)])";
        rangePattern = Pattern.compile( rangePatternString, Pattern.CASE_INSENSITIVE );
        pairPattern = Pattern.compile( "(" + versionPatternString + "),(" + versionPatternString + ")",
            Pattern.CASE_INSENSITIVE );
        leftPattern = Pattern.compile( "(" + versionPatternString + "),", Pattern.CASE_INSENSITIVE );
        rightPattern = Pattern.compile( ",(" + versionPatternString + ")", Pattern.CASE_INSENSITIVE );
        allPattern = Pattern.compile( ",", Pattern.CASE_INSENSITIVE );
        multiRangePattern = Pattern.compile( "(" + rangePatternString + "),(.*)", Pattern.CASE_INSENSITIVE );
    }

    public PomExplorer( String ... scopes )
    {
        this.scopes = scopes;
    }

    // ============================================================
    // Methods
    // ============================================================

    // ----------
    // public
    // ----------

    public void setVerbose( boolean verbose )
    {
        this.verbose = verbose;
    }

    public DepTree execute( File inProjectRootDir, Collection<DepRepo> inRepos, DepDirFilter inDirFilter )
        throws Exception
    {
        assert inProjectRootDir != null : "projectRootDir can not be null.";
        assert inRepos != null : "repos can not be null.";

        projectRootDir = inProjectRootDir;
        dirFilter = inDirFilter;
        repos = new ArrayList<DepRepo>( inRepos );
        missingDependencies = new HashSet<String>();
        properties = new HashMap<String, PomReader.Property>();
        layers = new Stack<DepLayer>();

        DepTree tree = new DepTree();

        //
        // Make a repo that represents the components in this project directory.
        //
        LocalRepo prjRepo = new LocalRepo( projectRootDir, PomReader.POM_FILENAME );
        repos.add( 0, prjRepo );

        //
        // Create an on-the-fly LocalRepo that represents all components in project directory.
        //
        DepLayer rootLayer = new DepLayer();
        layers.push( rootLayer );

        for ( File file : findProjectPomFiles( projectRootDir ) )
        {
            properties.clear();

            DepComponent component = readPom( new FileInputStream( file ) );
            DepDependency dep = new DepDependency();
            dep.setName( component.getName() );
            dep.setVersionPattern( "0" );
            component.setSeedDependency( dep );
            component.setRepo( prjRepo );

            DepNode node = new DepNode( component );
            rootLayer.addNode( node );

            prjRepo.addComponent( component, file.getParentFile() );
        }

        //
        // Walk breadth-first through the dependencies.
        //
        while ( !layers.peek().isEmpty() )
        {
            // Continue until there is nothing to do
            processLayer();
        }

        //
        // Convert layers into tree.
        //
        int depth = 0;
        HashMap<String, DepComponent> componentMap = new HashMap<String, DepComponent>();
        for ( DepLayer layer : layers )
        {
            for ( DepNode node : layer.getNodes() )
            {
                DepComponent component = node.readSelectedComponent();
                componentMap.put( component.getName(), component );
                if ( depth == 0 )
                {
                    tree.addRootComponent( component );
                }

                if ( node.getParent() != null )
                {
                    DepComponent parentComponent = componentMap.get( node.getParent().getName() );
                    parentComponent.addChild( component );
                }
            }

            depth++;
        }

        return tree;
    }

    // ----------
    // private
    // ----------

    private void processLayer()
        throws Exception
    {
        DepLayer prevLayer = layers.peek();
        DepLayer currLayer = new DepLayer();
        layers.push( currLayer );

        for ( DepNode parentNode : prevLayer.getNodes() )
        {
            DepComponent parentComponent = parentNode.readSelectedComponent();
            if ( parentComponent == null )
            {
                // System.out.println( "========= LOST COMPONENT " + parentNode.getName() + "  " + layers.size() );
                continue;
            }

            Collection<DepDependency> deps = new ArrayList<DepDependency>( parentComponent.getDependencies() );
            for ( DepDependency dep : deps )
            {
                //
                // Lookup dependency info in template dependencies if no version is specified.
                //
                if ( dep.getVersionPattern() == null )
                {
                    if ( verbose )
                    {
                        System.out.println( "  find version for [" + dep.getName() + "]" );
                    }

                    DepDependency templateDep = parentNode.getTemplateDependency( dep.getName() );
                    if ( templateDep != null )
                    {
                        dep.setVersionPattern( templateDep.getVersionPattern() );

                        if ( dep.getScope() == null )
                        {
                            dep.setScope( templateDep.getScope() );
                        }

                        if ( !dep.isOptional() && templateDep.isOptional() )
                        {
                            dep.setOptional( true );
                        }
                    }
                    else
                    {
                        throw new Exception( "Unable to resolve version for [" + dep.getName() + "] within [" +
                            parentNode + "]" );
                    }
                }

                // Assume don't care about optionals.
                if ( dep.isOptional() )
                {
                    continue;
                }

                Scope scope = Scope.find( dep.getScope() );
                boolean scopeFound = false;
                for ( String s : scopes )
                {
                    if ( scope.matches( s ) )
                    {
                        scopeFound = true;
                        break;
                    }
                }

                // If the scope is not in list reject the dependency.
                if ( !scopeFound )
                {
                    continue;
                }

                //
                // Find Node
                //
                FindResult result = findOrCreateNode( parentNode, dep );

                switch ( result.getStatus() )
                {
                    case newComponent:
                        DepNode node = result.getNode();

                        currLayer.addNode( node );
                        break;

                    case missingComponent:
                        missingDependencies.add( dep.getName() );
                        dep.setMissing( true );
                        break;

                    case redundantComponent:
                        ArrayList<DepRepoVersion> oldVersions = new ArrayList<DepRepoVersion>( result.getNode().versions );
                        boolean selectedInvalid = result.getNode().filterVersions( dep.getVersionPattern() );

                        // If there are no versions left in the node, something is seriously wrong. Stick with
                        // whatever was there before.
                        if ( result.getNode().hasNoVersions() )
                        {
                            result.getNode().versions = oldVersions;
                            System.err.println( "There are no versions that will work for " +
                                result.getNode().getName() + ". Last filter: " + dep.getVersionPattern() );
//                            throw new Exception( "There are no versions that will work for " +
//                                result.getNode().getName() + ". Last filter: " + dep.getVersionPattern() );
                            break;
                        }

                        // If the selected version of this component is now invalid, must backup to that layer!
                        if ( selectedInvalid )
                        {
                            while ( !layers.peek().containsNode( result.getNode().getName() ) )
                            {
                                layers.pop();
                            }

                            // Backed up to the layer that had a conflict. Return to start over.
                            return;
                        }
                        break;
                }
            }
        }
    }

    /**
     * Search directory for any pom files to define root components.
     */
    private List<File> findProjectPomFiles( File dir )
        throws Exception
    {
        ArrayList<File> depDefFiles = new ArrayList<File>();

        for ( File file : dir.listFiles( new DepDefFilenameFilter() ) )
        {
            if ( file.isDirectory() )
            {
                if ( dirFilter == null || dirFilter.isDepSearchDir( file ) )
                {
                    depDefFiles.addAll( findProjectPomFiles( file ) );
                }
            }
            else
            {
                depDefFiles.add( file );
            }
        }

        return depDefFiles;
    }

    /**
     * Warning, this closes InputStream.
     */
    private DepComponent readPom( InputStream in )
        throws Exception
    {
        PomReader reader = new PomReader();
        reader.read( in );
        in.close();

        DepComponent component = reader.getComponent();

        //
        // Read template for properties and dependencies.
        //
        DepDependency templateDep = reader.getTemplate();
        if ( templateDep != null )
        {
            DepNode templateNode = new DepNode( null, templateDep );
            DepComponent templateComponent = templateNode.readSelectedComponent();

            // If the template exists, read it.
            if ( templateComponent != null )
            {
                addProperty( new PomReader.Property( "parent.version",
                    resolveProperties( templateComponent.getVersionNumber() ) ) );

                // Add any dependencies in template to this component.
                for ( DepDependency dep : templateComponent.getDependencies() )
                {
                    component.addDependency( dep );
                }

                // Add template dependencies to this component
                for ( DepDependency dep : templateComponent.getTemplateDependencies() )
                {
                    component.addTemplateDependency( dep );
                }
            }
        }

        //
        // Resolve variables.
        //
        for ( PomReader.Property prop : reader.getProperties() )
        {
            addProperty( prop );
        }

        addProperty( new PomReader.Property( "project.groupId", resolveProperties( PomReader.getGroupId( component
            .getName() ) ) ) );
        addProperty( new PomReader.Property( "pom.groupId", resolveProperties( PomReader.getGroupId( component
            .getName() ) ) ) );
        addProperty( new PomReader.Property( "project.artifactId",
            resolveProperties( PomReader.getArtifactId( component.getName() ) ) ) );

        component.setVersionNumber( resolveProperties( component.getVersionNumber() ) );
        addProperty( new PomReader.Property( "project.version", component.getVersionNumber() ) );
        addProperty( new PomReader.Property( "pom.version", component.getVersionNumber() ) );

        for ( DepDependency dep : component.getTemplateDependencies() )
        {
            dep.setName( resolveProperties( dep.getName() ) );
            dep.setVersionPattern( resolveProperties( dep.getVersionPattern() ) );
        }

        for ( DepDependency dep : component.getDependencies() )
        {
            dep.setName( resolveProperties( dep.getName() ) );
            dep.setVersionPattern( resolveProperties( dep.getVersionPattern() ) );
        }

        return component;
    }

    private boolean addProperty( PomReader.Property prop )
    {
        // Can override properties??
        // if ( properties.get( prop.getKey() ) != null )
        // {
        // return false;
        // }

        properties.put( prop.getKey(), prop );

        return true;
    }

    private DepNode findExistingNode( String name )
        throws Exception
    {
        assert name != null : "DepDependency name can not be null";

        for ( DepLayer layer : layers )
        {
            if ( layer.containsNode( name ) )
            {
                return layer.getNode( name );
            }
        }

        return null;
    }

    private FindResult findOrCreateNode( DepNode parent, DepDependency dep )
        throws Exception
    {
        assert dep.getName() != null : "DepDependency name can not be null";

        //
        // If already visited, return old node.
        //
        DepNode node = findExistingNode( dep.getName() );
        if ( node != null )
        {
            if ( verbose )
            {
                System.out.println( "  redundantComponent " + node.getName() );
            }

            return new FindResult( Status.redundantComponent, node );
        }

        //
        // New node not in any previous layer.
        //
        node = new DepNode( parent, dep );
        if ( verbose )
        {
            System.out.println( "  node " + node );
        }

        if ( node.hasNoVersions() )
        {
            return new FindResult( Status.missingComponent, node );
        }

        return new FindResult( Status.newComponent, node );
    }

    private MatchType matches( String inVersionNumber, String inPatternString )
        throws Exception
    {
        if ( inPatternString == null )
        {
            return MatchType.match;
        }

        String versionNumber = cleanVersionNumber( inVersionNumber );
        String patternString = cleanVersionNumber( inPatternString );

        if ( patternString.equalsIgnoreCase( "*" ) || patternString.equalsIgnoreCase( "RELEASE" ) ||
            patternString.equalsIgnoreCase( "LATEST" ) )
        {
            return MatchType.match;
        }

        // Specified a specific number: 1.2.3-4.
        if ( versionPattern.matcher( patternString ).matches() )
        {
            return sameVersion( patternString, versionNumber ) ? MatchType.suggested : MatchType.match;
        }

        // Range [1.0.0-1] or (1.0.0-1,2.0.0-1) or (1.0.0-1,) or (,2.0.0-1)
        MatchType rangeMatch = matchesRange( patternString, versionNumber );
        if ( rangeMatch != null )
        {
            return rangeMatch;
        }

        // Multi-range
        Matcher m = multiRangePattern.matcher( patternString );
        if ( m.matches() )
        {
            String extra = patternString;
            while ( m.matches() )
            {
                m = multiRangePattern.matcher( extra );
                if ( m.matches() )
                {
                    String range = m.group( 1 );
                    extra = m.group( 5 );

                    MatchType match = matchesRange( range, versionNumber );
                    if ( match != MatchType.noMatch )
                    {
                        return match;
                    }
                }
            }

            return matchesRange( extra, versionNumber );
        }

        return MatchType.noMatch;
    }

    private MatchType matchesRange( String range, String versionNumber )
        throws Exception
    {
        Matcher m = rangePattern.matcher( range );
        if ( !m.matches() )
        {
            return null;
        }

        String opLeft = m.group( 1 );
        String innerString = m.group( 2 );
        String opRight = m.group( 3 );

        // Exact: [1.2.3-4]
        if ( versionPattern.matcher( innerString ).matches() )
        {
            if ( !opLeft.equalsIgnoreCase( "[" ) || !opRight.equalsIgnoreCase( "]" ) )
            {
                throw new Exception( "Single version in range must be contained in []" );
            }

            return sameVersion( innerString, versionNumber ) ? MatchType.exact : MatchType.noMatch;
        }

        // Pair: [1.0.0-1,2.0.0-1]
        m = pairPattern.matcher( innerString );
        if ( m.matches() )
        {
            String beginVersion = m.group( 1 );
            String endVersion = m.group( 2 );

            if ( versionBefore( beginVersion, versionNumber ) )
            {
                if ( opLeft.equalsIgnoreCase( "[" ) )
                {
                    return sameVersion( beginVersion, versionNumber ) ? MatchType.match : MatchType.noMatch;
                }
                else
                {
                    return MatchType.noMatch;
                }
            }

            if ( versionAfter( endVersion, versionNumber ) )
            {
                if ( opRight.equalsIgnoreCase( "]" ) )
                {
                    return sameVersion( endVersion, versionNumber ) ? MatchType.match : MatchType.noMatch;
                }
                else
                {
                    return MatchType.noMatch;
                }
            }

            return MatchType.match;
        }

        // Lower Limit: [1.0.0-1,)
        m = leftPattern.matcher( innerString );
        if ( m.matches() )
        {
//            if ( !opRight.equalsIgnoreCase( ")" ) )
//            {
//                throw new Exception( "Upper limit version range must end with a ) not a ]." );
//            }

            String beginVersion = m.group( 1 );

            if ( versionBefore( beginVersion, versionNumber ) )
            {
                if ( opLeft.equalsIgnoreCase( "[" ) )
                {
                    return sameVersion( beginVersion, versionNumber ) ? MatchType.match : MatchType.noMatch;
                }
                else
                {
                    return MatchType.noMatch;
                }
            }

            return MatchType.match;
        }

        // Upper Limit: (,2.0.0-1]
        m = rightPattern.matcher( innerString );
        if ( m.matches() )
        {
            if ( !opLeft.equalsIgnoreCase( ")" ) )
            {
                throw new Exception( "Upper limit version range must start with a ( not a [." );
            }

            String endVersion = m.group( 1 );

            if ( versionAfter( endVersion, versionNumber ) )
            {
                if ( opLeft.equalsIgnoreCase( "]" ) )
                {
                    return sameVersion( endVersion, versionNumber ) ? MatchType.match : MatchType.noMatch;
                }
                else
                {
                    return MatchType.noMatch;
                }
            }

            return MatchType.match;
        }

        // No Limit: (,)
        m = allPattern.matcher( innerString );
        if ( m.matches() )
        {
            return MatchType.match;
        }

        return MatchType.noMatch;
    }

    private String cleanVersionNumber( String v )
    {
        return v.replaceAll( " ", "" ).replaceAll( "\n", "" ).replaceAll( "\r", "" ).replaceAll( "\t", "" )
            .replaceAll( "-", "." ).replaceAll( "_", "." ).trim().toLowerCase();
    }

    private boolean sameVersion( String version0, String version1 )
    {
        return version0.equals( version1 );
    }

    private boolean versionBefore( String version0, String version1 )
        throws Exception
    {
        String[] v0 = version0.split( "\\." );
        String[] v1 = version1.split( "\\." );
        int count = v0.length > v1.length ? v0.length : v1.length;

        for ( int i = 0; i < count; i++ )
        {
            int n0 = i >= v0.length ? 0 : Integer.parseInt( v0[i] );
            int n1 = i >= v1.length ? 0 : Integer.parseInt( v1[i] );

            if ( n1 < n0 )
            {
                return true;
            }
            else if ( n1 > n0 )
            {
                return false;
            }
        }

        return true;
    }

    private boolean versionAfter( String version0, String version1 )
        throws Exception
    {
        return versionBefore( version1, version0 );
    }

    private String getPomFileName( DepRepoVersion version )
        throws Exception
    {
        if ( version == null )
        {
            return PomReader.POM_FILENAME;
        }

        if ( version.getRepo() instanceof LocalRepo )
        {
            return PomReader.POM_FILENAME;
        }

        return String.format( "%s-%s%s", PomReader.getArtifactId( version.getComponentName() ),
            version.getVersionNumber(), PomReader.POM_EXT );
    }

    private String resolveProperties( String string )
    {
        if ( string == null )
        {
            return null;
        }

        if ( !string.contains( "${" ) )
        {
            return string;
        }

        // // People make mistakes with / instead of .
        // string = string.replace( "/", "." );
        //
        // // People added spaces
        // string = string.replace( " ", "" );

        for ( PomReader.Property prop : properties.values() )
        {
            String propString = "${" + prop.getKey() + "}";
            if ( string.contains( propString ) )
            {
                string = string.replace( propString, prop.getValue() );
            }
        }

        return string;
    }

    // ============================================================
    // Inner Classes
    // ============================================================

    private static class DepDefFilenameFilter implements FilenameFilter
    {
        @Override
        public boolean accept( File dir, String name )
        {
            try
            {
                File file = new File( dir, name );

                if ( file.isDirectory() )
                {
                    return true;
                }

                return file.getName().equalsIgnoreCase( PomReader.POM_FILENAME ) ||
                    file.getName().toLowerCase().endsWith( PomReader.POM_EXT );
            }
            catch ( Exception ex )
            {
                throw new RuntimeException( ex );
            }
        }
    }

    private class DepLayer
    {
        private ArrayList<DepNode> nodes;
        private HashMap<String, DepNode> map;

        public DepLayer()
        {
            nodes = new ArrayList<DepNode>();
            map = new HashMap<String, DepNode>();
        }

        public void addNode( DepNode node )
        {
            nodes.add( node );
            map.put( node.getName(), node );
        }

        public List<DepNode> getNodes()
        {
            return nodes;
        }

        public boolean containsNode( String name )
        {
            return map.containsKey( name );
        }

        public DepNode getNode( String name )
        {
            return map.get( name );
        }

        public boolean isEmpty()
        {
            return nodes.isEmpty();
        }
    }

    /**
     * Represents a group of component versions.
     */
    private class DepNode
    {
        private DepDependency seedDependency;
        private ArrayList<DepRepoVersion> versions;
        private DepRepoVersion selectedVersion;
        private DepNode parent;
        private DepComponent selectedComponent;

        public DepNode()
        {
            versions = new ArrayList<DepRepoVersion>();
        }

        /**
         * Local file root layer constructor.
         */
        public DepNode( DepComponent component )
        {
            this();

            selectedComponent = component;
            seedDependency = component.getSeedDependency();
            versions.add( new DepRepoVersion( component.getRepo(), component.getName(), component.getVersionNumber() ) );
            selectedVersion = versions.get( 0 );
        }

        public DepNode( DepNode inParent, DepDependency dep )
            throws Exception
        {
            this();

            parent = inParent;
            seedDependency = dep;

            //
            // Search repos in order.
            //
            selectedVersion = null;
            for ( DepRepo repo : repos )
            {
                if ( verbose )
                {
                    System.out.println( "    " + repo );
                }

                List<DepRepoVersion> allVersions = repo.getVersions( dep.getName() );

                if ( allVersions == null || allVersions.isEmpty() )
                {
                    continue;
                }

                // For each repo, get list of versions (Latest to Earliest).
                for ( DepRepoVersion version : allVersions )
                {
                    if ( verbose )
                    {
                        System.out.println( "    version " + version );
                    }

                    //
                    // Include only versions that match dependency version pattern.
                    //
                    MatchType type = matches( version.getVersionNumber(), dep.getVersionPattern() );

                    if ( type != MatchType.noMatch )
                    {
                        versions.add( version );
                    }

                    if ( type == MatchType.suggested )
                    {
                        selectedVersion = version;
                    }
                }

                // Only use the first repo found.
                break;
            }

            // If nothing was specifically selected, just pick the latest (first).
            if ( selectedVersion == null )
            {
                selectedVersion = versions.isEmpty() ? null : versions.get( 0 );
            }
        }

        public String getName()
        {
            return seedDependency.getName();
        }

        public boolean hasNoVersions()
        {
            return versions.isEmpty();
        }

        public DepComponent readSelectedComponent()
            throws Exception
        {
            if ( selectedVersion == null )
            {
                return null;
            }

            try
            {
                InputStream in = selectedVersion.getRepo().getFileEntryInputStream( seedDependency.getName(),
                    selectedVersion.getVersionNumber(), getPomFileName( selectedVersion ) );
                properties.clear();
                selectedComponent = readPom( in );

                selectedComponent.setRepo( selectedVersion.getRepo() );
                selectedComponent.setSeedDependency( seedDependency );
                // Override the version read from depDefFile just in case there is an incompatibility.
                selectedComponent.setVersionNumber( selectedVersion.getVersionNumber() );
            }
            catch ( Exception ex )
            {
                System.err.println( ex );
                if ( verbose )
                {
                    System.err.printf( "Unable to read component [%s/%s/%s]\n", seedDependency,
                        selectedVersion.getVersionNumber(), getPomFileName( selectedVersion ) );
                }

                return null;
            }

            return selectedComponent;
        }

        public boolean filterVersions( String versionPattern )
            throws Exception
        {
            if ( versions.isEmpty() )
            {
                return true;
            }

            ArrayList<DepRepoVersion> oldVersions = versions;
            versions = new ArrayList<DepRepoVersion>();
            for ( DepRepoVersion version : oldVersions )
            {
                if ( matches( version.getVersionNumber(), versionPattern ) != MatchType.noMatch )
                {
                    versions.add( version );
                }
            }

            if ( !versions.contains( selectedVersion ) )
            {
                // Selected version is no longer in the list, must pick the new first in list.
                selectedVersion = versions.isEmpty() ? null : versions.get( 0 );

                return true;
            }

            return false;
        }

        public DepNode getParent()
        {
            return parent;
        }

        public DepDependency getTemplateDependency( String name )
        {
            DepDependency dep = selectedComponent.getTemplateDependency( name );
            if ( dep != null )
            {
                return dep;
            }

            if ( parent == null )
            {
                return null;
            }

            return parent.getTemplateDependency( name );
        }

        @Override
        public String toString()
        {
            return String.format( "{name=%s,versions=%s}", seedDependency.getName(), versions );
        }
    }
    
    private static class FindResult
    {
        private Status status;
        private DepNode node;

        public FindResult( Status status, DepNode node )
        {
            this.status = status;
            this.node = node;
        }

        public DepNode getNode()
        {
            return node;
        }

        public Status getStatus()
        {
            return status;
        }
    }
}
